<?php

// ------------------------------------------------------------------------
// |@Author       : Jarmin <topextend@qq.com>
// |@----------------------------------------------------------------------
// |@Date         : 2025-05-04 23:29:01
// |@----------------------------------------------------------------------
// |@LastEditTime : 2025-05-10 02:26:45
// |@----------------------------------------------------------------------
// |@LastEditors  : Jarmin <topextend@qq.com>
// |@----------------------------------------------------------------------
// |@Description  :
// |@----------------------------------------------------------------------
// |@FilePath     : Controller.php
// |@----------------------------------------------------------------------
// |@Copyright (c) 2025 http://www.ladmin.cn All Rights Reserved.
// ------------------------------------------------------------------------
declare (strict_types=1);

namespace think\admin;

use think\App;
use think\Request;
use think\Response;
use ReflectionMethod;
use think\admin\Validate;
use think\admin\traits\ResponseTrait;
use think\exception\ValidateException;
use think\admin\exception\DataException;
use think\admin\exception\AuthException;
use think\admin\exception\BusinessException;

abstract class Controller
{
    use ResponseTrait;

    /**
     * App 实例
     * @var App
     */
    protected App $app;

    /**
     * 请求实例
     * @var Request
     */
    protected Request $request;

    /**
     * Service 实例
     * @var Service
     */
    protected Service $service;

    /**
     * 模块名称
     * @var string
     */
    protected string $module;

    /**
     * 控制器名称
     * @var string
     */
    protected string $controller;

    /**
     * 动作名称
     * @var string
     */
    protected string $action;

    /**
     * 是否批量验证
     * @var bool
     */
    protected bool $batchValidate = false;

    /**
     * 构造方法
     * @param App $app
     */
    public function __construct(App $app)
    {
        $this->initializeProperties($app);
        $this->initializeValidation();
        $this->initialize();
    }

    /**
     * 初始化基本属性
     * @param App $app
     */
    protected function initializeProperties(App $app): void
    {
        $this->app = $app;
        $this->request = $app->request;
        $this->module = $app->http->getName();
        $this->controller = $this->request->controller();
        $this->action = $this->request->action();
    }

    /**
     * 初始化验证
     */
    protected function initializeValidation(): void
    {
        // 自动签名验证
        $this->autoSign();
        // 自动参数验证
        $this->autoValidate();
    }

    /**
     * 初始化方法
     * 子类必须实现此方法以初始化 service 属性
     * @return void
     */
    abstract protected function initialize(): void;

    /**
     * 执行控制器方法并捕获异常
     * @param callable $callback 控制器方法回调
     * @return Response
     */
    protected function handleException(callable $callback): Response
    {
        try {
            return $callback();
        } catch (ValidateException $e) {
            return $this->validateError($e->getMessage());
        } catch (AuthException $e) {
            return $this->unauthorized($e->getMessage());
        } catch (BusinessException|DataException $e) {
            return $this->error($e->getMessage(), $e->getCode());
        } catch (\Exception $e) {
            $message = $this->app->isDebug() ? $e->getMessage() : '服务器错误';
            $data = $this->app->isDebug() ? [
                'file' => $e->getFile(),
                'line' => $e->getLine(),
                'trace' => $e->getTraceAsString()
            ] : null;
            return $this->error($message, self::SYSTEM_ERROR, $data);
        }
    }

    /**
     * 解析方法注解
     * 检查指定方法是否存在特定注解
     * @param string $annotation 要检查的注解名称（不包含@符号）
     * @return bool|string 是否存在指定注解
     */
    protected function parseMethodAnnotation(string $annotation, bool $getValue = false): bool|string
    {
        // 使用双层缓存：方法缓存 + 注解缓存
        static $annotationCache = [];
        $methodKey = "{$this->module}.{$this->controller}.{$this->action}";

        // 如果该方法的注解缓存不存在，则初始化
        if (!isset($annotationCache[$methodKey])) {
            $annotationCache[$methodKey] = [];
        }

        // 如果该方法的特定注解缓存不存在，则解析并缓存
        if (!isset($annotationCache[$methodKey][$annotation])) {
            $reflectionMethod = $this->getReflectionMethod();
            $methodDocComment = $reflectionMethod->getDocComment();

            // 解析注解值
            if ($methodDocComment !== false) {
                preg_match("/@{$annotation}\s+(\w+)/i", $methodDocComment, $matches);
                $annotationCache[$methodKey][$annotation] = !empty($matches[1]) ? $matches[1] : true;
            } else {
                $annotationCache[$methodKey][$annotation] = false;
            }
        }

        return $getValue ? $annotationCache[$methodKey][$annotation] : (bool)$annotationCache[$methodKey][$annotation];
    }

    /**
     * 获取注解的值
     * @param string $annotation 要获取值的注解名称
     * @param mixed $default 默认值
     * @return mixed 注解值或默认值
     */
    protected function getAnnotationValue(string $annotation, $default = null): mixed
    {
        $value = $this->parseMethodAnnotation($annotation, true);
        return $value === true ? $default : $value;
    }

    /**
     * 获取当前请求的反射方法
     * 使用缓存机制优化反射性能
     * @return ReflectionMethod 当前请求方法的反射实例
     */
    protected function getReflectionMethod(): ReflectionMethod
    {
        // 使用应用级静态缓存而非请求级缓存
        static $methodCache = [];
        $key = "{$this->module}.{$this->controller}.{$this->action}";
        if (!isset($methodCache[$key])) {
            $controllerClass = $this->app->parseClass('controller', $this->controller);
            $methodCache[$key] = new ReflectionMethod($controllerClass, $this->action);
        }

        return $methodCache[$key];
    }

    /**
     * 自动签名验证
     *
     * 签名验证流程：
     * 1. 过滤空值参数
     * 2. 检查签名参数是否存在
     * 3. 验证用户登录状态
     * 4. 生成服务端签名并与客户端签名比对
     *
     * @throws BusinessException|AuthException 验证失败时抛出异常
     */
    protected function autoSign(): void
    {
        $authMode = $this->getAnnotationValue('auth');
        // 当@auth注解存在且不等于'sign'时跳过验证
        if ($authMode !== 'sign') {
            return;
        }
        if ($this->request->isGet()) {
            return;
        }

        $param = array_filter($this->request->param(), function ($value) {
            return $value !== "" && $value !== null;
        });

        if (!array_key_exists('sign', $param)) {
            throw new BusinessException('缺少签名参数', self::PARAM_ERROR);
        }

        unset($param['sign']);

        if (!$this->request->user) {
            throw new AuthException('登录已过期');
        }

        $param['salt'] = $this->request->user['salt'] ?? '';
        ksort($param, SORT_NATURAL);

        foreach ($param as &$v) {
            if (is_array($v)) {
                $v = str_replace('\/', '/', json_encode($v, JSON_UNESCAPED_UNICODE));
            }
            if (is_bool($v)) {
                $v = $v ? "true" : "false";
            }
        }

        $sign = hash('sha256', join(',', $param));
        $clientSign = $this->request->param('sign');

        if ($clientSign !== $sign) {
            throw new BusinessException('签名无效', self::SIGN_INVALID);
        }
    }

    /**
     * 自动参数验证
     * 根据方法注解自动验证请求参数：
     * - @validate true: 启用基础验证
     * - @mixed validate true: 根据id参数区分添加/编辑场景
     * - @password validate true: 根据password参数区分批量/单个操作
     * @throws BusinessException|ValidateException 验证失败时抛出异常
     */
    protected function autoValidate(): void
    {
        if ($this->request->isGet()) {
            return;
        }
        // 检查方法注解是否存在 @validate 注解
        $authMode = $this->getAnnotationValue('validate');
        if (!in_array($authMode, ['true', 'mixed', 'password'])) {
            return;
        }

        $param = $this->request->param();
        $validateClass = $this->app->parseClass('validate', $this->controller);
        if (!class_exists($validateClass)) {
            throw new BusinessException(
                lang('system.validate_not_exists', ['class' => $validateClass]),
                self::CLASS_NOT_EXISTS
            );
        }

        $validate = $this->app->make($validateClass);
        $scene = $this->getValidateScene($authMode, $param);

        if (!$validate->hasScene($scene)) {
            throw new BusinessException(
                lang('system.validate_scene_not_found', ['class' => $validateClass, 'scene' => "scene{$scene}"]),
                self::METHOD_NOT_EXISTS
            );
        }

        if (!$validate->scene($scene)->check($param)) {
            throw new ValidateException($validate->getError());
        }
    }

    /**
     * 获取验证场景
     */
    protected function getValidateScene(string $authMode, array $param): string
    {
        $scene = ucfirst($this->action);

        if ($authMode === 'mixed') {
            $scene = empty($param['id']) ? "{$this->action}Add" : "{$this->action}Edit";
        } elseif ($authMode === 'password') {
            $scene = empty($param['password']) ? "{$this->action}Batch" : "{$this->action}";
        }

        return $scene;
    }

    /**
     * 验证数据
     * @access protected
     * @param  array        $data     数据
     * @param  string|array $validate 验证器名或者验证规则数组
     * @param  array        $message  提示信息
     * @param  bool         $batch    是否批量验证
     * @return array|string|true
     * @throws ValidateException
     */
    protected function validate(array $data, string|array $validate, array $message = [], bool $batch = false)
    {
        if (is_array($validate)) {
            $v = new Validate();
            $v->rule($validate);
        } else {
            if (strpos($validate, '.')) {
                // 支持场景
                [$validate, $scene] = explode('.', $validate);
            }
            $class = false !== strpos($validate, '\\') ? $validate : $this->app->parseClass('validate', $validate);
            $v     = new $class();
            if (!empty($scene)) {
                $v->scene($scene);
            }
        }

        $v->message($message);

        // 是否批量验证
        if ($batch || $this->batchValidate) {
            $v->batch(true);
        }

        return $v->failException(true)->check($data);
    }
}
